Если вы не изучали JavaScript с самого начала, то осваивать его современную версию сложно. Экосистема быстро растёт и меняется, так что трудно разобраться с проблемами, для решения которых придуманы разные инструменты. Я начал программировать в 1998-м, но начал понимать JavaScript только в 2014-м. Помню, как просматривал Browserify и смотрел на его слоган:
Browserify позволяет делать require («модули») в браузере, объединяя все ваши зависимости
Я не понял ни слова из предложения и стал разбираться, как это может помочь мне как разработчику.
Цель статьи — рассказать о контексте, в котором инструменты в JavaScript развивались вплоть до 2017-го. Начнём с самого начала и будем делать сайт, как это делали бы динозавры — безо всяких инструментов, на чистом HTML и JavaScript. Постепенно станем вводить разные инструменты, поочерёдно рассматривая решаемые ими проблемы. Благодаря историческому контексту вы сможете адаптироваться к постоянно меняющемуся ландшафту JavaScript и понять его.
Олдскульное использование JavaScript
Давайте сделаем «олдскульный» сайт, используя только HTML и JavaScript. В этом случае придётся вручную скачивать и связывать файлы. Вот простой index.html, ссылающийся на JavaScript-файл:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript Example</title>
<script src="index.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>
Строка <script src="index.js"></script>
ссылается на JavaScript-файл index.js, находящийся в той же директории:
// index.js
console.log("Hello from JavaScript!");
Больше для создания сайта ничего не нужно! Допустим, вы хотите добавить стороннюю библиотеку moment.js (меняет формат дат для удобочитаемости). Можно, к примеру, использовать в JS функцию moment
:
moment().startOf('day').fromNow(); // 20 часов назад
Но так вы всего лишь добавили moment.js на свой сайт! На главной странице moment.js приведены инструкции:
Мда, в разделе «Установка» много всего написано. Но пока это проигнорируем, потому что можно добавить moment.js на сайт, скачав файл moment.min.js в ту же директорию и включив его в наш файл index.html.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example</title>
<link rel="stylesheet" href="index.css">
<script src="moment.min.js"></script>
<script src="index.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>
Обратите внимание, что moment.min.js загружается до index.js, поэтому в index.js вы можете использовать функцию moment
:
// index.js
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());
Вот так мы создавали сайты с JS-библиотеками. Процедура была простой для понимания, но раздражала необходимость искать и скачивать новые версии библиотек при каждом их обновлении.
Использование диспетчера пакетов из JavaScript (npm)
Примерно с 2010-го развиваются несколько конкурирующих диспетчеров пакетов, помогающих автоматизировать скачивание и обновление библиотек из центрального репозитория. Bower был самым популярным в 2013-м, но к 2015-му уступил пальму первенства npm. Надо сказать, что с конца 2016-го yarn широко используется в качестве альтернативы интерфейсу npm, но под капотом он всё ещё работает с npm-пакетами.
Изначально npm создавался как диспетчер пакетов специально для node.js, среды исполнения JavaScript, предназначенной для серверов, а не фронтенда. Так что довольно странно применять его в качестве диспетчера пакетов для библиотек, запускаемых в браузерах.
Примечание: обычно диспетчеры пакетов подразумевают использование командной строки, что раньше никогда не требовалось при разработке фронтенда. Если вы никогда с ней не работали, то для начала можете почитать это руководство. Как бы там ни было, в современном JavaScript важно уметь пользоваться командной строкой (и это также открывает двери в другие области разработки).
Давайте посмотрим, как использовать npm для автоматической установки moment.js вместо скачивания вручную. Если у вас установлен node.js, то у вас уже есть и npm, так что можете в командной строке перейти в папку с файлом index.html и ввести:
$ npm init
Вам зададут несколько вопросов (можно просто жать Enter, оставляя ответы по умолчанию), а потом сгенерируется новый файл package.json. Это конфигурационный файл, в котором npm сохраняет всю информацию о проекте. По умолчанию содержимое package.json выглядит так:
{
"name": "your-project-name",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Для установки JS-пакета moment.js можно воспользоваться инструкциями с сайта npm, введя в командной строке:
$ npm install moment --save
Эта команда делает две вещи:
- Скачивает весь код из пакета moment.js в папку под названием node_modules.
- Автоматически модифицирует файл package.json для отслеживания moment.js в качестве проектной зависимости.
{
"name": "modern-javascript-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.19.1"
}
}
Это полезно, когда вы станете работать над проектом совместно с другими разработчиками: вместо общего доступа к папке node_modules (которая может быть очень большой) достаточно открыть доступ к файлу package.json, и другие разработчики смогут автоматически устанавливать нужные пакеты с помощью команды npm install
.
Теперь нам больше не нужно вручную скачивать moment.js с сайта, npm помогает скачивать и обновлять автоматически. Если посмотрим в папку node_modules, то в директории node_modules/moment/min увидим файл moment.min.js. Это означает, что в index.html можно сослаться на скачанную через npm версию moment.min.js:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript Example</title>
<script src="node_modules/moment/min/moment.min.js"></script>
<script src="index.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>
Приятно, что теперь мы можем автоматически скачивать и обновлять наши пакеты с помощью npm и командной строки. Но теперь надо копаться в папке node_modules, чтобы узнать местонахождение каждого пакета и вручную прописать в HTML. Это довольно неудобно, так что давайте посмотрим, как можно автоматизировать этот процесс.
Использование бандлера (bundler) JavaScript-модулей (webpack)
В большинстве языков программирования есть возможность импортирования кода из одного файла в другой. Изначально в JS такой возможности не было, потому что этот язык разрабатывался только для исполнения в браузере, без доступа к файловой системе на клиентской машине (по причинам безопасности). Так что долгое время для организации JS-кода в нескольких файлах требовалось загружать каждый файл с глобально доступными переменными.
Именно это мы и делали в вышеописанном примере с moment.js example — весь файл moment.min.js загружается в HTML, где определяется глобальная переменная moment, которая потом становится доступна любому файлу, загруженному после moment.min.js (вне зависимости от того, нужна ли она для обращения к ним).
В 2009-м был запущен проект CommonJS, в рамках которого планировалось создать спецификации внебраузерной экосистемы для JavaScript. Большая часть CommonJS была посвящена спецификациям модулей, позволявших JS импортировать и экспортировать код между файлами как во многих других языках, без обращения к глобальным переменным. Самой известной реализацией модулей CommonJS стал node.js.
Как уже говорилось, node.js — это среда исполнения JavaScript, разработанная для запуска на серверах. Вот как сначала выглядело использование node.js-модулей: вместо загрузки всего moment.min.js в скриптовом теге HTML можно было грузить JS-файл напрямую:
// index.js
var moment = require('moment');
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());
Загрузка модулей работает прекрасно, поскольку node.js — это серверный язык с доступом к файловой системе. Также ему известно расположение всех npm-модулей, поэтому вместо require('./node_modules/moment/min/moment.min.js)
можно писать просто require('moment')
.
Всё это прекрасно, но если вы попробуете использовать приведённый код в браузере, то получите ошибку, в которой говорится, что require
не определён. У браузера нет доступа к файловой системе, поэтому такая загрузка модулей реализована очень хитро: файлы нужно грузить динамически, синхронно (замедляет исполнение) или асинхронно (могут быть проблемы с синхронизацией).
И здесь появляется бандлер (bundler). Это инструмент для сборки модулей в единые пакеты, имеющий доступ к файловой системе. Получающиеся пакеты совместимы с браузером, которому не нужен доступ к файловой системе. В нашем случае бандлер нужен для поиска всех выражений require
(имеющих ошибочный, с точки зрения браузера, JS-синтаксис) и замены на настоящее содержимое каждого требуемого файла. В финале мы получаем единый JS-файл без выражений require
!
Самым популярным бандлером сначала был Browserify, выпущенный в 2011 г. Он был пионером в использовании node.js-выражений require
во фронтенде (это позволило npm стать самым востребованным диспетчером пакетов). К 2015-му лидером стал webpack (ему помогла популярность фронтенд-фреймворка React, использующего все возможности этого бандлера).
Давайте посмотрим, как с помощью webpack заставить работать в браузере наш пример с require('moment')
. Сначала установим бандлер в проект. Сам по себе webpack — это npm-пакет, так что введём в командной строке:
$ npm install webpack --save-dev
Обратите внимание на аргумент --save-dev
— он сохраняет бандлер как зависимость среды разработки, так что она не понадобится на production-сервере. Это отразится в файле package.json, который обновится автоматически:
{
"name": "modern-javascript-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.19.1"
},
"devDependencies": {
"webpack": "^3.7.1"
}
}
Теперь webpack установлен в качестве одного из пакетов в папке node_modules. Можно запускать его из командной строки:
$ ./node_modules/.bin/webpack index.js bundle.js
Эта команда запускает webpack вместе с файлом index.js, находит все выражения require
и заменяет соответствующим кодом, чтобы получился единый выходной файл bundle.js. Это означает, что нам больше не нужно использовать в браузере файл index.js, поскольку он содержит некорректные выражения require
. Вместо него мы возьмём bundle.js, что должно быть учтено в index.html:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript Example</title>
<script src="bundle.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>
Если обновите браузер, то всё будет работать, как и прежде.
Обратите внимание, что нам нужно выполнять webpack-команду при каждом изменении index.js. Это утомительно, и чем более продвинутые возможности webpack мы будем использовать (вроде генерирования схемы источников для отладки исходного кода из транспилированного), тем больше станет утомлять постоянный ввод команд. Webpack может считывать опции из конфигурационного файла webpack.config.js в корневой директории проекта:
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js'
}
};
Теперь при каждом изменении index.js можно запускать webpack такой командой:
$ ./node_modules/.bin/webpack
Больше не нужно определять опции index.js
и bundle.js
, потому что webpack загружает их из webpack.config.js. Так лучше, но всё равно утомительно вводить эту команду при каждом изменении кода. Надо ещё больше всё упростить.
Такой подход очень сильно повлиял на рабочий процесс. Мы уже не загружаем внешние скрипты с помощью глобальных переменных. Все новые JS-библиотеки будут добавлены в JavaScript с помощью выражений require
, а не через теги <script>
в HTML. Наличие единственного JS-пакета зачастую повышает производительность. А после добавления этапа сборки появилось несколько мощных возможностей, которые тоже можно использовать в процессе разработки.
Транспилирование кода ради новых возможностей языка (babel)
Транспилирование — это конвертация кода в другой, похожий язык. Это важная часть фронтенд-разработки: поскольку в браузерах медленно появляются новые фичи, были созданы языки с экспериментальными возможностями, которые транспилируются в совместимые с браузерами языки.
К примеру, для CSS есть Sass, Less и Stylus. Для JavaScript самым популярным транспилятором какое-то время был CoffeeScript (выпущен около 2010), а сегодня многие используют babel или TypeScript. CoffeeScript улучшает JavaScript за счёт серьёзного изменения языка — опциональное использование скобок, значимые отступы (whitespace) и т. д. Babel — это не новый язык, а транспилятор, который транспилирует JavaScript следующего поколения, имеющего возможности, пока недоступные во всех браузерах (ES2015 и выше), в старый и более совместимый JavaScript (ES5). Typescript — это язык, по существу аналогичный JavaScript следующего поколения, но с добавлением опциональной статичной типизации. Многие предпочитают babel, потому что он ближе к ванильному JavaScript.
Рассмотрим пример использования babel на этапе webpack-сборки. Сначала установим транспилятор (это npm-пакет) в проект:
$ npm install babel-core babel-preset-env babel-loader --save-dev
Обратите внимание, что мы установили три отдельных пакета в качестве зависимостей среды разработки:
babel-core
— основная часть babel;babel-preset-env
— пресет, определяющий, какие новые возможности JavaScript нужно транспилировать;babel-loader
— пакет, позволяющий babel работать с webpack.
Сконфигурируем webpack для использования babel-loader
, отредактировав файл webpack.config.js:
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
}
};
Синтаксис может вас запутать (к счастью, этот код не нужно часто редактировать). По сути, мы просим webpack найти все .js-файлы (за исключением лежащих в папке node_modules) и применить babel-транспилирование с помощью babel-loader
и пресета babel-preset-env
. Подробнее о синтаксисе конфигурирования webpack можно почитать здесь.
Всё настроено, можно использовать в нашем JavaScript возможности ES2015! Пример шаблонной строки (template string) ES2015 в файле index.js:
// index.js
var moment = require('moment');
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());
var name = "Bob", time = "today";
console.log(`Hello ${name}, how are you ${time}?`);
Для загрузки модулей можем воспользоваться выражением импортирования ES2015 вместо require
, сегодня это встречается во многих кодовых базах:
// index.js
import moment from 'moment';
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());
var name = "Bob", time = "today";
console.log(`Hello ${name}, how are you ${time}?`);
В этом примере синтаксис import
мало отличается от синтаксиса require
, но import
гибче в более сложных ситуациях. Раз мы изменили index.js, нужно снова запустить webpack:
$ ./node_modules/.bin/webpack
Теперь обновим index.html в браузере. Когда я писал эту статью, большинство браузеров поддерживали все возможности ES2015, так что трудно сказать, заслуга ли это babel. Можете протестировать в старых браузерах вроде IE9 или поискать в bundle.js строку транспилированного кода:
// bundle.js
// ...
console.log('Hello ' + name + ', how are you ' + time + '?');
// ...
Здесь babel транспилировал шаблонную строку ES2015 в обычное JavaScript-объединение строк, чтобы сохранить совместимость. Наверное, не самый впечатляющий пример, но транспилирование кода — очень мощный инструмент. В JavaScript можно добавить такие впечатляющие возможности, как async/await. И хотя транспилирование иногда бывает нудным и неприятным занятием, в последние годы оно помогло сильно улучшить язык, потому что многие разработчики сегодня тестируют возможности будущего.
Мы почти закончили, но в нашем рабочем процессе ещё остались шероховатости. Ради повышения производительности нужно минифицировать получившийся после сборки бандлером файл, но это довольно простая задача. Также нужно перезапускать webpack при каждом изменении JavaScript, который быстро устаревает. Рассмотрим инструменты для решения этих проблем.
Использование средства запуска задач (task runner) (npm-скрипты)
Раз мы используем сборку при работе с модулями, имеет смысл подумать о средстве запуска задач — инструменте автоматизации разных операций процесса сборки. Во фронтенд-разработке это минификация, оптимизация изображений, прогон тестов и т. д.
В 2013-м самым популярным инструментом был Grunt, но вскоре его место занял Gulp. Оба используют плагины, играющие роль обёртки для других инструментов, которым нужна командная строка. Сегодня чаще всего применяется скриптинг, встроенный в npm, который напрямую работает с упомянутыми инструментами.
Давайте напишем npm-скрипт для упрощения работы с webpack. Для этого просто изменим package.json:
{
"name": "modern-javascript-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --progress -p",
"watch": "webpack --progress --watch"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.19.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"webpack": "^3.7.1"
}
}
Здесь мы добавили два новых скрипта — build
и watch
. Для запуска сборочного скрипта введите команду:
$ npm run build
Команда запускает webpack (с использованием конфигурации из сделанного ранее webpack.config.js) с опцией --progress
, которая включает отображение прогресса в процентах, и с опцией -p
для минимизации кода. Запустим скрипт watch
:
$ npm run watch
Здесь используется опция --watch
для автоматического перезапуска webpack при каждом изменении JavaScript-файла, это очень удобно для разработки.
Обратите внимание, что package.json может запускать webpack без указания полного пути ./node_modules/.bin/webpack, потому что node.js знает расположение каждого npm-модуля. Удобно! Сделаем ещё удобнее, установив webpack-dev-server, отдельный простой веб-сервер с функцией горячей перезагрузки. Установим в качестве зависимости среды разработки:
$ npm install webpack-dev-server --save-dev
Теперь добавим npm-скрипт в package.json:
{
"name": "modern-javascript-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --progress -p",
"watch": "webpack --progress --watch",
"server": "webpack-dev-server --open"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.19.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"webpack": "^3.7.1"
}
}
Можно запускать сервер разработки:
$ npm run server
Команда автоматически открывает index.html в браузере по адресу localhost:8080 (по умолчанию). При изменении JavaScript-кода в index.js webpack-dev-server будет пересобирать свой собранный в пакет JavaScript и автоматически обновлять браузер. Это действительно экономит время, потому что можно сосредоточиться на коде, а не постоянно переключать внимание с кода на браузер, чтобы просматривать изменения.
Есть много других опций для webpack и webpack-dev-server, мы лишь прошлись по самым верхушкам (подробнее тут). Также вы можете написать npm-скрипты для других задач вроде конвертирования Sass в CSS, сжатия изображений, запуска тестов — всего, что использует командную строку. Также много классных советов можно почерпнуть из выступления Кейт Хадсон:
Заключение
Мы вкратце рассмотрели современный JavaScript. Прошли от сочетания чистых HTML и JS до использования:
- диспетчера пакетов для автоматической загрузки сторонних пакетов;
- бандлера для создания единых файлов скриптов;
- транспилятора для использования будущих возможностей JS;
- и средства запуска задач для автоматизации разных операций в процессе сборки.
Конечно, здесь много куда можно двигаться, особенно новичкам. Для них веб-разработка раньше была прекрасной отправной точкой, потому что можно было легко начать и втянуться в дело. А сегодня это уже не такое простое занятие, в частности из-за разнообразия быстро сменяющих друг друга инструментов.
Но всё не так плохо, как может показаться. Ситуация устаканивается, node-экосистема всё больше воспринимается как жизнеспособный вариант работы с фронтендом. Удобство и согласованность в работе обеспечивается использованием npm в качестве диспетчера пакетов, node-выражений require
или import
для работы с модулями и npm-скриптов для запуска задач. И этот рабочий процесс гораздо проще, чем было год-два назад!
Фреймворки сегодня часто поставляются с инструментами, облегчающими начало веб-разработки. В Ember есть ember-cli
, оказавший огромное влияние на angular-cli
из Angular, create-react-app
из React, vue-cli
из Vue и т. д. Эти инструменты обеспечат ваш проект всем необходимым для того, чтобы начать писать код. Но они не волшебные палочки, они лишь правильно всё настраивают для комфортной работы. Нередко вам может понадобиться дополнительно настроить конфигурацию webpack, babel и т. д. И очень важно понимать, что делает каждый из инструментов.
С современным JavaScript может быть тяжело работать, потому что он продолжает быстро меняться и развиваться. Но даже если иногда это похоже на изобретение колеса, быстрая эволюция языка помогает продвигать инновации вроде горячей перезагрузки, линтинга в реальном времени и отладки в режиме «машины времени». Сегодня интересно быть разработчиком, и я надеюсь, что эта статья поможет вам спланировать свой собственный путь.